<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Storage 合约交互工具</title>
  <!-- Vue 3 CDN -->
  <script src="js/vue.global.prod.js"></script>
  <!-- Ethers v5 CDN -->
  <script src="js/ethers.umd.min.js"></script>
  <style>
    :root {
      --bg:#0b0e14; --card:#11151f; --muted:#9aa4b2; --text:#e6eaf2; --accent:#4f8cff; --ok:#20c997; --warn:#ffc107; --err:#ff6b6b;
    }
    * { box-sizing: border-box; }
    body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, 'Helvetica Neue', Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji"; background: var(--bg); color: var(--text); }
    a{ color: var(--accent); }
    .wrap { max-width: 1100px; margin: 24px auto; padding: 0 16px; }
    .grid { display:grid; grid-template-columns: repeat(12, 1fr); gap:16px; }
    .card { background: var(--card); border:1px solid #1f2430; border-radius:16px; padding:16px; box-shadow: 0 6px 24px rgba(0,0,0,.25); }
    .row { display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
    .row > * { flex: 1 1 auto; }
    .btn { background: var(--accent); color:#fff; border:none; padding:10px 14px; border-radius:12px; cursor:pointer; font-weight:600; }
    .btn:disabled { opacity:.6; cursor:not-allowed; }
    .btn.secondary { background:#2a2f3b; color:var(--text); }
    .btn.warn { background: var(--warn); color:#222; }
    .btn.ok { background: var(--ok); }
    .input, select { width:100%; padding:10px 12px; border-radius:12px; border:1px solid #293140; background:#0f1320; color:var(--text); outline: none; }
    .label { font-size:12px; color: var(--muted); margin-bottom:6px; }
    .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
    .kvs { display:grid; grid-template-columns: 200px 1fr; gap:10px; }
    .pill { font-size: 12px; border:1px solid #2a3040; border-radius:999px; padding:4px 10px; color: var(--muted); }
    .footer { text-align:center; color: var(--muted); margin-top: 18px; }
    .title { font-size:20px; font-weight:800; margin:0 0 8px; }
    .subtitle { color: var(--muted); margin-bottom: 12px; }
    .hr { height:1px; background:#1e2433; margin: 10px 0 16px; border:0; }
    .badge-ok{ color: var(--ok); }
    .badge-err{ color: var(--err); }
    code { background:#0f1320; border:1px solid #21283a; padding:2px 6px; border-radius:6px; }
    @media (max-width: 900px) {
      .kvs { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
  <div id="app" class="wrap">
    <div class="grid">
      <!-- 连接钱包和合约加载 -->
      <div class="card" style="grid-column: span 12;">
        <div class="row" style="justify-content: space-between; align-items: center; gap:8px">
          <div>
            <div class="title">Storage 合约交互工具</div>
          </div>
          <div class="row" style="gap:8px; flex:0 0 auto; align-items: center">
            <span class="pill">网络：<span class="mono">{{ prettyChain(chainId || '未知') }}</span></span>
            <input class="input mono" placeholder="合约地址" v-model.trim="contractAddress" style="width: 400px;" />
            <button class="btn" @click="loadContract" :disabled="!connected || !contractAddress">加载合约</button>
            <button class="btn" @click="connect" :disabled="connecting">{{ connected? short(account): (connecting? '连接中…':'连接钱包') }}</button>
            <span class="pill" v-if="contractLoaded">已加载 Storage 合约</span>
          </div>
        </div>
        <div class="subtitle" v-if="contractError">
          <span class="badge-err">⚠</span> {{ contractError }}
        </div>
      </div>

      <!-- 左侧面板：存储功能 -->
      <div class="card" style="grid-column: span 6;" v-if="contractLoaded">
        <div class="title">存储功能</div>
        <div class="kvs mono">
          <div>当前存储值</div><div>{{ currentValue }}</div>
        </div>
        <hr class="hr">

        <!-- 存储新值 -->
        <div style="margin-bottom: 16px;">
          <div class="label">存储新值 (store)</div>
          <div class="row">
            <div style="flex:1">
              <input class="input mono" v-model.trim="storeValue" placeholder="要存储的数字" type="number" />
            </div>
            <div style="flex:0 0 auto">
              <button class="btn ok" @click="doStore" :disabled="!storeValue">存储</button>
            </div>
          </div>
          <div class="subtitle" v-if="storeHash">
            交易：<a :href="explorerTx(storeHash)" target="_blank" rel="noreferrer">{{ short(storeHash) }}</a>
          </div>
        </div>

        <!-- 刷新当前值 -->
        <div>
          <div class="label">读取当前值 (retrieve)</div>
          <div class="row">
            <div style="flex:1">
              <button class="btn secondary" @click="doRetrieve">刷新当前值</button>
            </div>
          </div>
          <div class="subtitle" style="margin-top: 8px;">
            点击按钮从合约中读取当前存储的值
          </div>
        </div>
      </div>

      <!-- 右侧面板：合约信息和操作历史 -->
      <div class="card" style="grid-column: span 6;" v-if="contractLoaded">
        <div class="title">合约信息</div>
        <div class="kvs mono">
          <div>合约地址</div><div>{{ contractAddress }}</div>
          <div>合约类型</div><div>Storage</div>
          <div>存储变量</div><div>uint256 number</div>
        </div>
        <hr class="hr">

        <!-- 操作历史 -->
        <div>
          <div class="label">最近操作</div>
          <div class="mono" style="white-space: pre-wrap; line-height:1.5; max-height: 200px; overflow:auto; background:#0f1320; border:1px solid #1f2434; border-radius:12px; padding:10px;">
            {{ operationHistory.join('\n') }}
          </div>
        </div>
      </div>

      <!-- 日志 -->
      <div class="card" style="grid-column: span 12;">
        <div class="title">日志</div>
        <div class="mono" style="white-space: pre-wrap; line-height:1.5; max-height: 260px; overflow:auto; background:#0f1320; border:1px solid #1f2434; border-radius:12px; padding:10px;">
{{ logs.join('\n') }}
        </div>
      </div>

      <div class="footer card" style="grid-column: span 12;">
        小贴士：
        <ul>
          <li>确保浏览器已安装 MetaMask 并在目标网络上；</li>
          <li>首次使用请点击"连接钱包"；</li>
          <li>存储操作需要支付 gas 费用，读取操作是免费的；</li>
          <li>Storage 合约只有一个 uint256 类型的存储变量。</li>
        </ul>
      </div>
    </div>
  </div>

<script>
const { createApp, reactive } = Vue

const STORAGE_ABI = [
  // Storage 合约的 ABI
  "function store(uint256 num) public",
  "function retrieve() public view returns (uint256)"
]

// 全局变量，避免 Vue 代理冲突
let _provider = null
let _signer = null
let _contract = null

createApp({
  data(){
    return {
      account: '',
      chainId: null,
      connected: false,
      connecting: false,

      contractAddress: '0x5fbdb2315678afecb367f032d93f642f64180aa3',
      contractLoaded: false,
      contractError: '',

      currentValue: '-',
      storeValue: '',

      storeHash: '',

      operationHistory: [],
      logs: []
    }
  },
  mounted(){
    if(window.ethereum){
      this.logs.push('检测到 window.ethereum，等待连接…')
      // 创建 ethers provider（使用全局变量）
      _provider = new ethers.providers.Web3Provider(window.ethereum)
      // 监听账户/网络变更
      window.ethereum.on?.('accountsChanged', (accs)=>{
        this.account = accs?.[0]||'';
        this.connected = !!this.account;
        _signer = _provider.getSigner()
        if(_contract) this.refreshCurrentValue()
      })
      window.ethereum.on?.('chainChanged', (cid)=>{
        this.chainId = cid;
        // 重新创建 provider 和 signer
        _provider = new ethers.providers.Web3Provider(window.ethereum)
        _signer = _provider.getSigner()
        if(_contract) this.loadContract()
      })
    } else {
      this.logs.push('未检测到 MetaMask：请安装扩展。')
    }
  },
  methods: {
    prettyChain(id){
      const map = { '0x1':'Ethereum', '0x5':'Goerli', '0xaa36a7':'Sepolia', '0x89':'Polygon', '0x38':'BSC', '0xa4b1':'Arbitrum', '0x2105':'Base' }
      return map[id] || String(id)
    },
    short(v){ return v? v.slice(0,6)+'…'+v.slice(-4) : '' },
    log(msg){ this.logs.unshift(`[${new Date().toLocaleTimeString()}] ${msg}`) },
    addOperation(op){ this.operationHistory.unshift(`[${new Date().toLocaleTimeString()}] ${op}`) },

    async connect(){
      try{
        if(!window.ethereum) throw new Error('未检测到 MetaMask')
        this.connecting = true

        // 使用 ethers provider 连接钱包
        _provider = new ethers.providers.Web3Provider(window.ethereum)
        const accounts = await _provider.send('eth_requestAccounts', [])
        this.account = accounts[0]
        _signer = _provider.getSigner()

        // 获取网络信息
        const network = await _provider.getNetwork()
        this.chainId = '0x' + network.chainId.toString(16)

        this.connected = true
        this.log(`已连接 ${this.account} @ chain ${this.chainId}`)
      }catch(err){
        this.log('连接失败：'+(err?.message||err))
      }finally{ this.connecting = false }
    },

    async loadContract(){
      this.contractError = ''
      try{
        if(!this.connected) await this.connect()
        if(!ethers.utils.isAddress(this.contractAddress)) throw new Error('非法合约地址')

        // 使用 ethers 创建合约对象
        _contract = new ethers.Contract(this.contractAddress, STORAGE_ABI, _provider)

        this.contractLoaded = true
        this.log(`已加载 Storage 合约 ${this.contractAddress}`)

        // 读取当前值
        await this.refreshCurrentValue()

      }catch(err){
        this.contractError = err?.message || String(err)
        this.log('加载合约失败：'+ this.contractError)
      }
    },

    async refreshCurrentValue(){
      if(!_contract) return
      try{
        const value = await _contract.retrieve()
        this.currentValue = value.toString()
        this.log(`当前存储值：${this.currentValue}`)
      }catch(err){
        this.currentValue = '-'
        this.log('读取当前值失败：'+(err?.message||err))
      }
    },

    async doStore(){
      try{
        if(!_contract) throw new Error('请先加载合约')
        const value = ethers.BigNumber.from(String(this.storeValue || '0'))

        // 使用带签名者的合约对象
        const contractWithSigner = _contract.connect(_signer)
        const t = await contractWithSigner.store(value)

        this.storeHash = t.hash
        this.log(`提交存储操作：${this.short(t.hash)}`)
        this.addOperation(`存储新值: ${this.storeValue}`)

        // 等待交易确认
        const receipt = await t.wait()
        this.log(`存储成功，区块：${receipt.blockNumber}`)

        // 刷新当前值
        await this.refreshCurrentValue()

        // 清空输入框
        this.storeValue = ''

      }catch(err){ this.log('存储失败：'+(err?.message||err)) }
    },

    async doRetrieve(){
      try{
        if(!_contract) throw new Error('请先加载合约')
        await this.refreshCurrentValue()
        this.addOperation(`读取当前值: ${this.currentValue}`)
      }catch(err){ this.log('读取失败：'+(err?.message||err)) }
    },

    explorerBase(){
      // 仅覆盖常见网络，其他网络可自己扩展
      const map = {
        '0x1':'https://etherscan.io',
        '0xaa36a7':'https://sepolia.etherscan.io',
        '0x5':'https://goerli.etherscan.io',
        '0x89':'https://polygonscan.com',
        '0x38':'https://bscscan.com',
        '0xa4b1':'https://arbiscan.io',
        '0x2105':'https://basescan.org'
      }
      return map[this.chainId] || 'https://etherscan.io'
    },
    explorerTx(hash){ return this.explorerBase()+ '/tx/' + hash },

  }
}).mount('#app')
</script>
</body>
</html>